//Paul Irish's requestAnimationFrame polyfill (slightly modyfied)
//http://paulirish.com/2011/requestanimationframe-for-smart-animating/
if (typeof window.requestAnimationFrame !== "function") {
	window.requestAnimationFrame = (function() {
		return window.webkitRequestAnimationFrame
			|| window.mozRequestAnimationFrame
			|| window.oRequestAnimationFrame
			|| window.msRequestAnimationFrame
			|| function(callback){
				window.setTimeout(callback, 1000 / 60);
			};
	}());
}

var 
	//regular expression for valid object ID
	idRegex = /[a-zA-Z_][a-zA-Z0-9_]*/,

	//constructor
	sCanvas = function(selector, ready, error) {
		//self-reference
		var loaded,
			sc = this;

		//check selector
		if (typeof selector !== "string" && !selector.nodeType) {
			throw new TypeError("Selector must be string or HTMLElement.");
		}

		//save selector
		this.selector = selector;

		//initialize fields to its default values
		this.initFields();

		//bind primary ready handler
		this.ready(this.init);

		//bind passed handlers
		if (typeof ready === "function") {
			this.ready(ready);
		}

		if (typeof error === "function") {
			this.error(error);
		}

		//if DOM was already loaded, fire ready immediatelly
		if (document.readyState === "complete") {
			this.ready();
		} else {
			loaded = function() {
				if (document.addEventListener) {
					document.removeEventListener("DOMContentLoaded", loaded, false);
					window.removeEventListener("load", loaded, false);
				} else {
					document.detachEvent("onreadystatechange", loaded);
					window.detachEvent("onload", loaded);
				}				
				sc.ready();
			};
			
			if (document.addEventListener) {
				//better way to detect DOM load
				document.addEventListener("DOMContentLoaded", loaded, false);

				//load will always work
				window.addEventListener("load", loaded, false);
			} else {
				//better way to detect DOM load
				document.attachEvent("onreadystatechange", loaded);
				
				//onload will always work
				window.attachEvent("onload", loaded);
			}

		}
	};

sCanvas.fn = sCanvas.prototype;

//extends first object with one or more others
sCanvas.extend = sCanvas.fn.extend = function() {
	var key, obj,
		prototypeExtended = false,
		result = arguments[0] || { },
		deep = false,
		i = 1;
	
	if (typeof result === "boolean") {
		deep = result;
		result = arguments[1] || { };
		i++;
	}
	
	//if only one object is passed, sCanvas will be extended
	if (arguments.length === i) {
		result = this;
		i--;
		
		if (this === sCanvas.fn) {
			prototypeExtended = true;
		}
	} else if (typeof result !== "object") {
		result = { };
	}
	
	for (; i < arguments.length; i++) {
		obj = arguments[i];
		if (obj !== undefined && obj !== null) {
			if (deep) {
				for (key in obj) {
					if (typeof obj[key] === "object" && obj[key] !== null) {
						//if extending prototype with object, defer it for object creation
						if (prototypeExtended) {
							sCanvas.fields[key] = sCanvas.fields[key] || (sCanvas.isArray(obj[key]) ? [] : {});
							sCanvas.extend(true, sCanvas.fields[key], obj[key]);
						} else {
							result[key] = result[key] || (sCanvas.isArray(obj[key]) ? [] : {});
							sCanvas.extend(true, result[key], obj[key]);
						}
					} else {
						result[key] = obj[key];
					}
				}
			} else {
				for (key in obj) {
					//if extending prototype with object, defer it for object creation
					if (prototypeExtended && typeof obj[key] === "object") {
						sCanvas.fields[key] = obj[key];
					} else {
						result[key] = obj[key];
					}
				}
			}
		}
	}

	return result;
};

sCanvas.extend({
	
	fields: { },
	
	//returns actuall time as a number
	now: function() {
		return (new Date).getTime();
	},

	//checks if passed argument is valid ID, returns true if it is valid
	isValidId: function(id) {
		return id.match(idRegex) !== null;
	},

	//checks if passed object is group
	isGroup: function(obj) {
		return (obj instanceof CanvasGroup);
	},
	
	//checks if passed object is Array
	isArray: function(obj) {
		if (typeof Array.isArray === "function") {
			return Array.isArray(obj);
		} else {
			return Object.prototype.toString.apply(obj, [ ]) === "[object Array]";
		}
	},
	
	//returns true if passed object is empty object or empty array
	isEmpty: function(obj) {
		var i, result = true;
		if (typeof obj === "object") {
			for (i in obj) {
				result = false;
				break;
			}
		} else {
			result = false;
		}
		return result;
	},
	
	//constructs event object
	Event: function(type, data) {
		return new EventObject(type, data);
	}
	
});

sCanvas.fn.extend(true, {

	//reassigns constructor
//		constructor: sCanvas,

	//stores original canvas selector passed to constructor
	selector: null,

	//stores elemement wrapping canvas
	wrapper: null,

	//stores front canvas
	front: null,

	//stores back canvas
	back: null,

	//stores context of front canvas
	frontCx: null,

	//stores context of back canvas
	backCx: null,

	//stores event handlers
	handlers: {
		ready: [ ],
		error: [ ]
	},

	//remembers if canvas is already ready
	isReady: false,

	//remembers if canvas is already initialized with error
	isError: false,

	//stores if canvas is autorepainting
	autorepainting: true,

	//stores canvas position in element
	pos: {
		top: 0,
		left: 0
	},
	
	//stores if canvas was already initialized
	initialized: false,
	
	support: {
		//isPointInPath correct support, including correct working with transformations
		isPointInPath: false
	},
	
	//initializes object fields
	initFields: function() {
		if (!this.initialized) {
			sCanvas.extend(true, this, sCanvas.fields);
			this.initialized = true;
		}
	},

	//initializes sCanvas
	//is invoked as first ready handler
	init: function(e) {
		var err = null;

		//get wrapper element and save it
		if (typeof this.selector === "string") {
			this.wrapper = document.getElementById(this.selector);
		} else if (this.selector.nodeType) {
			this.wrapper = this.selector;
		}

		//get canvas - do not raise error when canvas was not 
		try {
			this.front = this.wrapper.getElementsByTagName("canvas")[0];
		} catch (err) {}

		//check if canvas was found
		if (!!this.front) {
			//get context and save it
			try {
				this.frontCx = this.front.getContext("2d");
			} catch (err) {}

			//check if context was created properly
			if (!!this.frontCx) {
				this.wrap();
				this.position();
				this.checkSupport();
			} else {
				this.frontCx = null;
				err = new Error("This browser does not support canvas.");
			}
		} else {
			err = new ReferenceError("Canvas element was not found.");
		}

		//if error occured, stop propagating ready event and trigger error event
		if (!!err) {
			e.stopImmediatePropagation();
			
			this.trigger(
				sCanvas.Event("error", {
					error: err
				}).removeHandlers()
			);
		} else {
			//sCanvas was initialized properly
			//so there is no need to store error handlers anymore
			this.handlers.error = [ ];
		}
	},

	//creates html structure, adds necessary styles
	wrap: function() {
		var wrapperStyles = window.getComputedStyle(this.wrapper);

		//make wrapper offset parent
		if (wrapperStyles.getPropertyValue("position") === "static") {
			this.wrapper.style.position = "relative";
		}

		//add canvas absolute position styles
		this.front.style.position = "absolute";
		this.front.style.top = "0px";
		this.front.style.left = "0px";
		this.front.style.zIndex = "2";

		//clone original canvas
		this.back = this.front.cloneNode(false);
		this.back.style.zIndex = "1";
		this.backCx = this.back.getContext("2d");
		this.wrapper.appendChild(this.back);
	},

	//counts canvas position in element
	position: function() {
		var left = 0,
			top = 0,
			obj = this.front;

		while (obj !== null) {
			left += obj.offsetLeft;
			top += obj.offsetTop;

			obj = obj.offsetParent;
		}

		this.pos = {
			left: left,
			top: top
		};
		
		return this.pos;
	},
	
	//check support for various functionalities
	checkSupport: function() {
		this.support.isPointInPath = this.checkSupportIsPointInPath();
	},

	//check support for isPointInPath
	checkSupportIsPointInPath: function() {
		var result = false,
			cx = this.frontCx;
		
		//check presence of isPointInPath method
		if (typeof this.frontCx.isPointInPath === "function") {
			//if present, prepare test-case - translated circle
			cx.save();
			cx.translate(20, 20);
			cx.beginPath();
			cx.arc(0, 0, 5, 0, 2 * Math.PI, false);
			cx.closePath();
			cx.restore();
			
			//check 9 points in and out of circle - only 4 corner points must be out
			if ((!cx.isPointInPath(16, 16) && !cx.isPointInPath(16, 24)
					&& !cx.isPointInPath(24, 24) && !cx.isPointInPath(24, 16)
					&& cx.isPointInPath(16, 20) && cx.isPointInPath(20, 16)
					&& cx.isPointInPath(20, 20) && cx.isPointInPath(20, 24)
					&& cx.isPointInPath(24, 20)) === true) {
				result = true;
			}
		}
		return result;
	},

	//registers ready event handler or triggers ready event
	ready: function(fn) {
		if (fn === undefined) {
			//triggering event
			this.trigger(
				sCanvas.Event("ready").removeHandlers()
			);

			//remember that ready event has been already triggered
			this.isReady = true;
		} else {
			//registering handler
			//if ready already occured, execute it immeditelly
			if (this.isReady) {
				this.bind("ready", fn).trigger(
					sCanvas.Event("ready").removeHandlers()
				);
			} else {
				this.bind("ready", fn);
			}
		}
		return this;
	},

	//registers error event handler or triggers error event
	error: function(fn) {
		if (fn === undefined) {
			//triggering event
			this.trigger(
				sCanvas.Event("error").removeHandlers()
			);

			//remember that error event has been already triggered
			this.isError = true;
		} else {
			//registering handler
			//if error already occured, execute it immeditelly
			if (this.isError) {
				this.bind("error", fn).trigger(
					sCanvas.Event("error").removeHandlers()
				);
			} else {
				this.bind("error", fn);
			}
		}
		return this;
	},

	//binds event handler
	bind: function(event, fn) {
		if (typeof fn !== "function") {
			throw new TypeError("Event handler was not passed.");
		}

		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		this.handlers[event].push(fn);

		return this;
	},

	//triggers event
	//remove is internal argument, should not be used externally
	trigger: function(event, data) {
		var args, e, i;
		
		//unify arguments
		if (event instanceof EventObject) {
			e = event;
			event = event.type;
		} else {
			e = sCanvas.Event(event);
		}

		//check if passed event type is supported
		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		args = [ e ];
		if (sCanvas.isArray(data)) {
			for (i = 0; i < data.length; i++) {
				args.push(data[i]);
			}
		}

		//execute all binded handlers
		for (i in this.handlers[event]) {
			this.handlers[event][i].apply(this, args);

			//if executing of rest of the handlers was stopped
			if (e.isImmediatePropagationStopped()) {
				break;
			}
		}

		//if remove is set to true, handlers are removed
		if (e.isRemovingHandlers()) {
			this.handlers[event] = [ ];
		}

		return this;
	},

	//performs repaint if autorepainting is on
	autorepaint: function() {
		if (this.isAutorepainting()) {
			this.repaint();
		}
	},

	//returns true if autorepainting is on
	isAutorepainting: function() {
		return this.autorepainting;
	},
	
	//turns autorepainting on
	//if passed argument is true, canvas is also repainted
	startAutorepainting: function(repaint) {
		this.autorepainting = true;
		
		if (repaint) {
			this.repaint();
		}
	},
	
	//turns autorepainting off
	stopAutorepainting: function() {
		this.autorepainting = false;
	},

	//repaints canvas
	repaint: function() {
		var obj;

		//TODO: think of some optimization to canvas clearing
		//oversizing - temporary fix for FF17
		this.frontCx.clearRect(-this.width(), -this.height(), 3 * this.width(), 3 * this.height());

		obj = this.bottom;
		while (obj !== null) {
			obj.draw(this.frontCx);
			obj = obj.overObj;
		}

		return this;
	},

	//gets/sets canvas width
	width: function(w) {
		if (w === undefined) {
			//getter
			return this.front.width;
		} else {
			//setter
			//TODO: think of performing repaint
			this.front.width = w;
			this.back.width = w;
			return this;
		}
	},

	//gets/sets canvas width
	height: function(h) {
		if (h === undefined) {
			//getter
			return this.front.height;
		} else {
			//setter
			//TODO: think of performing repaint
			this.front.height = h;
			this.back.height = h;
			return this;
		}
	}

});
